import 'dart:ui' show lerpDouble;

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:kq_flutter_core_widget/utils/kq_screen_util.dart';

import '../../resources/kq_theme_colors.dart';
import '../../theme/kq_theme_manager.dart';

/// 通用开关按钮
///
/// @author 周卓
///
/// 在系统的按钮基础上支持自定义按钮大小
///
class KqSwitchButton extends StatefulWidget {
  /// 开或关
  final bool value;

  /// 变化监听
  final ValueChanged<bool>? onChanged;

  /// 是否禁用触摸和点击，一般需要点击外部整条Item来控制开关时需要设置为true，默认false
  final bool disabled;

  /// 按钮大小
  final Size? size;

  /// 打开时的背景颜色
  final Color? onBgColor;

  /// 关闭时的背景颜色
  final Color? offBgColor;

  /// 关闭时的边框颜色
  final Color? offBorderColor;

  /// 圆球的颜色
  final Color? thumbColor;

  const KqSwitchButton(
      {super.key,
      required this.value,
      this.onChanged,
      this.disabled = false,
      this.size,
      this.onBgColor,
      this.offBgColor,
      this.offBorderColor,
      this.thumbColor});

  @override
  State<KqSwitchButton> createState() => _KqSwitchButtonState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagProperty('value',
        value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>(
        'onChanged', onChanged,
        ifNull: 'disabled'));
  }
}

class _KqSwitchButtonState extends State<KqSwitchButton>
    with TickerProviderStateMixin {
  late TapGestureRecognizer _tap;
  late HorizontalDragGestureRecognizer _drag;

  late AnimationController _positionController;
  late CurvedAnimation position;

  late AnimationController _reactionController;
  late Animation<double> _reaction;

  bool get isInteractive => widget.onChanged != null && !widget.disabled;

  // A non-null boolean value that changes to true at the end of a drag if the
  // switch must be animated to the position indicated by the widget's value.
  bool needsPositionAnimation = false;

  late double trackWidth;
  late double trackHeight;
  late double trackRadius;
  late double trackInnerStart;
  late double trackInnerEnd;
  late double trackInnerLength;
  late double switchWidth;
  late double switchHeight;
  late double thumbRadius;

  @override
  void initState() {
    switchWidth = widget.size?.width ?? 72.r;
    switchHeight = widget.size?.height ?? 40.r;

    trackWidth = switchWidth - 8.r;
    trackHeight = switchHeight - 8.r;
    trackRadius = trackHeight / 2.0;
    trackInnerStart = trackHeight / 2.0;
    trackInnerEnd = trackWidth - trackInnerStart;
    trackInnerLength = trackInnerEnd - trackInnerStart;
    thumbRadius = trackRadius - 1.5.r;

    super.initState();

    _tap = TapGestureRecognizer()
      ..onTapDown = _handleTapDown
      ..onTapUp = _handleTapUp
      ..onTap = _handleTap
      ..onTapCancel = _handleTapCancel;
    _drag = HorizontalDragGestureRecognizer()
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
      ..onEnd = _handleDragEnd
      ..dragStartBehavior = DragStartBehavior.start;

    _positionController = AnimationController(
      duration: _kToggleDuration,
      value: widget.value ? 1.0 : 0.0,
      vsync: this,
    );
    position = CurvedAnimation(
      parent: _positionController,
      curve: Curves.linear,
    );
    _reactionController = AnimationController(
      duration: _kReactionDuration,
      vsync: this,
    );
    _reaction = CurvedAnimation(
      parent: _reactionController,
      curve: Curves.ease,
    );
  }

  @override
  void didUpdateWidget(KqSwitchButton oldWidget) {
    super.didUpdateWidget(oldWidget);
    _drag.dragStartBehavior = DragStartBehavior.start;

    if (needsPositionAnimation || oldWidget.value != widget.value) {
      _resumePositionAnimation(isLinear: needsPositionAnimation);
    }
  }

  // `isLinear` must be true if the position animation is trying to move the
  // thumb to the closest end after the most recent drag animation, so the curve
  // does not change when the controller's value is not 0 or 1.
  //
  // It can be set to false when it's an implicit animation triggered by
  // widget.value changes.
  void _resumePositionAnimation({bool isLinear = true}) {
    needsPositionAnimation = false;
    position
      ..curve = isLinear ? Curves.linear : Curves.ease
      ..reverseCurve = isLinear ? Curves.linear : Curves.ease.flipped;
    if (widget.value) {
      _positionController.forward();
    } else {
      _positionController.reverse();
    }
  }

  void _handleTapDown(TapDownDetails details) {
    if (isInteractive) {
      needsPositionAnimation = false;
    }
    _reactionController.forward();
  }

  void _handleTap() {
    if (isInteractive) {
      widget.onChanged!(!widget.value);
      _emitVibration();
    }
  }

  void _handleTapUp(TapUpDetails details) {
    if (isInteractive) {
      needsPositionAnimation = false;
      _reactionController.reverse();
    }
  }

  void _handleTapCancel() {
    if (isInteractive) {
      _reactionController.reverse();
    }
  }

  void _handleDragStart(DragStartDetails details) {
    if (isInteractive) {
      needsPositionAnimation = false;
      _reactionController.forward();
      _emitVibration();
    }
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    if (isInteractive) {
      position
        ..curve = Curves.linear
        ..reverseCurve = Curves.linear;
      final double delta = details.primaryDelta! / trackInnerLength;
      switch (Directionality.of(context)) {
        case TextDirection.rtl:
          _positionController.value -= delta;
          break;
        case TextDirection.ltr:
          _positionController.value += delta;
          break;
      }
    }
  }

  void _handleDragEnd(DragEndDetails details) {
    // Deferring the animation to the next build phase.
    setState(() {
      needsPositionAnimation = true;
    });
    // Call onChanged when the user's intent to change value is clear.
    if (position.value >= 0.5 != widget.value) {
      widget.onChanged!(!widget.value);
    }
    _reactionController.reverse();
  }

  void _emitVibration() {
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        HapticFeedback.lightImpact();
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    if (needsPositionAnimation) {
      _resumePositionAnimation();
    }
    return MouseRegion(
      cursor: isInteractive && kIsWeb
          ? SystemMouseCursors.click
          : MouseCursor.defer,
      child: Opacity(
        opacity: 1.0,
        child: _KqSwitchButtonRenderObjectWidget(
          value: widget.value,
          activeColor: widget.onBgColor ??
              (KqThemeManager.instance
                      .getConfig()
                      .commonConfig
                      .mainLightColor ??
                  KqThemeColors.textLightBlue),
          trackColor: widget.offBgColor ?? KqThemeColors.bgD8,
          thumbColor: widget.thumbColor ?? KqThemeColors.bgWhite,
          offBorderColor: widget.offBorderColor ?? KqThemeColors.bgTransparent,
          onChanged: widget.onChanged,
          disabled: widget.disabled,
          switchWidth: switchWidth,
          switchHeight: switchHeight,
          trackRadius: trackRadius,
          thumbRadius: thumbRadius,
          trackWidth: trackWidth,
          trackHeight: trackHeight,
          trackInnerStart: trackInnerStart,
          trackInnerEnd: trackInnerEnd,
          textDirection: Directionality.of(context),
          state: this,
        ),
      ),
    );
  }

  @override
  void dispose() {
    _tap.dispose();
    _drag.dispose();

    _positionController.dispose();
    _reactionController.dispose();
    super.dispose();
  }
}

class _KqSwitchButtonRenderObjectWidget extends LeafRenderObjectWidget {
  const _KqSwitchButtonRenderObjectWidget(
      {required this.value,
      required this.activeColor,
      required this.trackColor,
      required this.thumbColor,
      required this.offBorderColor,
      required this.onChanged,
      required this.textDirection,
      required this.state,
      required this.disabled,
      required this.trackWidth,
      required this.trackHeight,
      required this.switchWidth,
      required this.switchHeight,
      required this.trackRadius,
      required this.thumbRadius,
      required this.trackInnerStart,
      required this.trackInnerEnd});

  final bool value;
  final Color activeColor;
  final Color trackColor;
  final Color thumbColor;
  final Color offBorderColor;
  final double switchWidth;
  final double switchHeight;
  final double trackWidth;
  final double trackHeight;
  final double thumbRadius;
  final double trackRadius;
  final double trackInnerStart;
  final double trackInnerEnd;
  final bool disabled;
  final ValueChanged<bool>? onChanged;
  final _KqSwitchButtonState state;
  final TextDirection textDirection;

  @override
  _RenderKqSwitchButton createRenderObject(BuildContext context) {
    return _RenderKqSwitchButton(
      value: value,
      activeColor: activeColor,
      trackColor: trackColor,
      thumbColor: thumbColor,
      offBorderColor: offBorderColor,
      switchWidth: switchWidth,
      switchHeight: switchHeight,
      trackRadius: trackRadius,
      trackInnerStart: trackInnerStart,
      trackInnerEnd: trackInnerEnd,
      trackWidth: trackWidth,
      trackHeight: trackHeight,
      thumbRadius: thumbRadius,
      disabled: disabled,
      onChanged: onChanged,
      textDirection: textDirection,
      state: state,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, _RenderKqSwitchButton renderObject) {
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..trackColor = trackColor
      ..thumbColor = thumbColor
      ..offBorderColor = offBorderColor
      ..onChanged = onChanged
      ..textDirection = textDirection;
  }
}

// Opacity of a disabled switch, as eye-balled from iOS Simulator on Mac.
const double _kKqSwitchButtonDisabledOpacity = 0.5;

const Duration _kReactionDuration = Duration(milliseconds: 200);
const Duration _kToggleDuration = Duration(milliseconds: 150);

class _RenderKqSwitchButton extends RenderConstrainedBox {
  _RenderKqSwitchButton({
    required bool value,
    required Color activeColor,
    required Color trackColor,
    required Color thumbColor,
    required Color offBorderColor,
    required this.switchWidth,
    required this.switchHeight,
    required this.trackWidth,
    required this.trackHeight,
    required this.trackInnerStart,
    required this.trackInnerEnd,
    required this.trackRadius,
    required this.thumbRadius,
    required this.disabled,
    ValueChanged<bool>? onChanged,
    required TextDirection textDirection,
    required _KqSwitchButtonState state,
  })  : assert(value != null),
        assert(activeColor != null),
        assert(state != null),
        _value = value,
        _activeColor = activeColor,
        _offBorderColor = offBorderColor,
        _trackColor = trackColor,
        _thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor, shadows: []),
        _onChanged = onChanged,
        _textDirection = textDirection,
        _state = state,
        super(
            additionalConstraints: BoxConstraints.tightFor(
                width: switchWidth, height: switchHeight)) {
    state.position.addListener(markNeedsPaint);
    state._reaction.addListener(markNeedsPaint);
  }

  final double switchWidth;
  final double switchHeight;
  final double trackWidth;
  final double trackHeight;
  final double trackRadius;
  final double trackInnerStart;
  final double trackInnerEnd;
  final double thumbRadius;
  final bool disabled;

  final _KqSwitchButtonState _state;

  bool get value => _value;
  bool _value;

  set value(bool value) {
    if (value == _value) {
      return;
    }
    _value = value;
    markNeedsSemanticsUpdate();
  }

  Color get activeColor => _activeColor;
  Color _activeColor;

  set activeColor(Color value) {
    if (value == _activeColor) {
      return;
    }
    _activeColor = value;
    markNeedsPaint();
  }

  Color get offBorderColor => _offBorderColor;
  Color _offBorderColor;

  set offBorderColor(Color value) {
    if (value == _offBorderColor) {
      return;
    }
    _offBorderColor = value;
    markNeedsPaint();
  }

  Color get trackColor => _trackColor;
  Color _trackColor;

  set trackColor(Color value) {
    if (value == _trackColor) {
      return;
    }
    _trackColor = value;
    markNeedsPaint();
  }

  Color get thumbColor => _thumbPainter.color;
  CupertinoThumbPainter _thumbPainter;

  set thumbColor(Color value) {
    if (value == thumbColor) {
      return;
    }
    _thumbPainter = CupertinoThumbPainter.switchThumb(color: value);
    markNeedsPaint();
  }

  ValueChanged<bool>? get onChanged => _onChanged;
  ValueChanged<bool>? _onChanged;

  set onChanged(ValueChanged<bool>? value) {
    if (value == _onChanged) {
      return;
    }
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
      markNeedsSemanticsUpdate();
    }
  }

  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;

  set textDirection(TextDirection value) {
    if (_textDirection == value) {
      return;
    }
    _textDirection = value;
    markNeedsPaint();
  }

  bool get isInteractive => onChanged != null && !disabled;

  @override
  bool hitTestSelf(Offset position) => true;

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is PointerDownEvent && isInteractive) {
      _state._drag.addPointer(event);
      _state._tap.addPointer(event);
    }
  }

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);

    if (isInteractive) {
      config.onTap = _state._handleTap;
    }

    config.isEnabled = isInteractive;
    config.isToggled = _value;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

    final double currentValue = _state.position.value;
    final double currentReactionValue = _state._reaction.value;

    final double visualPosition;
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - currentValue;
        break;
      case TextDirection.ltr:
        visualPosition = currentValue;
        break;
    }

    final Paint paint = Paint()
      ..color = Color.lerp(trackColor, activeColor, currentValue)!;

    final Rect trackRect = Rect.fromLTWH(
      offset.dx + (size.width - trackWidth) / 2.0,
      offset.dy + (size.height - trackHeight) / 2.0,
      trackWidth,
      trackHeight,
    );
    final RRect trackRRect =
        RRect.fromRectAndRadius(trackRect, Radius.circular(trackRadius));
    canvas.drawRRect(trackRRect, paint);

    final Paint borderPaint = Paint()
      ..color = Color.lerp(KqThemeColors.bgD8, activeColor, currentValue)!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.5.r;
    canvas.drawRRect(trackRRect, borderPaint);

    final double currentThumbExtension =
        CupertinoThumbPainter.extension * currentReactionValue;
    final double thumbLeft = lerpDouble(
      trackRect.left + trackInnerStart - thumbRadius,
      trackRect.left + trackInnerEnd - thumbRadius - currentThumbExtension,
      visualPosition,
    )!;
    final double thumbRight = lerpDouble(
      trackRect.left + trackInnerStart + thumbRadius + currentThumbExtension,
      trackRect.left + trackInnerEnd + thumbRadius,
      visualPosition,
    )!;
    final double thumbCenterY = offset.dy + size.height / 2.0;
    final Rect thumbBounds = Rect.fromLTRB(
      thumbLeft,
      thumbCenterY - thumbRadius,
      thumbRight,
      thumbCenterY + thumbRadius,
    );

    _clipRRectLayer.layer = context
        .pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect,
            (PaintingContext innerContext, Offset offset) {
      _thumbPainter.paint(innerContext.canvas, thumbBounds);
    }, oldLayer: _clipRRectLayer.layer);
  }

  final LayerHandle<ClipRRectLayer> _clipRRectLayer =
      LayerHandle<ClipRRectLayer>();

  @override
  void dispose() {
    _clipRRectLayer.layer = null;
    super.dispose();
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(FlagProperty('value',
        value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    description.add(FlagProperty('isInteractive',
        value: isInteractive,
        ifTrue: 'enabled',
        ifFalse: 'disabled',
        showName: true,
        defaultValue: true));
  }
}
